iOS ☞ SDWebimage 内存暴增问题

您所在的位置:网站首页 sdwebimage加载大图片 内存增长 iOS ☞ SDWebimage 内存暴增问题

iOS ☞ SDWebimage 内存暴增问题

2024-03-26 10:09| 来源: 网络整理| 查看: 265

前言

相信很多开发都用过 SDWebimage 来解决 UITableView/UICollectionView 滑动卡顿等问题,而且很多公司在面试的时候都会被问到 SDWebimage 运行流程等问题。

运行流程

以最常用的 UIImageView 为例:

UIImageView+WebCache:setImageWithURL:placeholderImage:options: 先显示 placeholderImage, 同时由 SDWebimageManager 根据 URL 在本地查找图片。SDWebimageManager: downloadWithURL:delegate:options:userInfo: SDWebImageManager 是将UIImageView+WebCache 同 SDImageCache 链接起来的类。SDImageCache:queryDiskCacheForKey:delegate:userInfo 用来根据 CacheKey 查找图片是否已经在缓存中。如果内存中已经有图片缓存,SDWebimageManager 会回调 SDImageCacheDelegate: imageCache:didFindImage:forKey:userInfo:而 UIImageView+WebCache 则回调 SDWebImageManagerDelegate: webImageManager:didFinishWithImage: 来显示图片。如果内存中没有图片缓存,那么生成 NSInvocationOperation 添加到队列,从硬盘查找图片是否已经被下载。根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 中进行的操作,所以回主线程进行结果回调 notifyDelegate:。如果上一操作从硬盘中读取到了图片,则将图片添加到内存缓存中(如果空闲内存过小,会先情况内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片如果没有从硬盘缓存目录中读取到图片,则说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。共享或重新生成一个下载器 SDWebimageDownloader 开始下载图片。图片下载有 NSURLSession 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败的状态connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebimageDecoder 做图片解码处理。图片解码处理在一个 NSOperationQueue 中完成,不会拖慢主线程UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘在单独的 NSInvocationOperation 中完成,避免拖慢主线程。如果是在 iOS 上运行,SDImageCache 在初始化的时候会注册 notification 到 UIApplicationDidReceiveMemoryWarningNotification 以及 UIApplicationWillTerminateNotification, 在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

以上就是SDWebImage运行的基本流程,开发者应该或多或少的了解过或者背过。那么现在有一个问题: 上述步骤中,哪些是卡UI的?如果将图片更换为从本地获取,UITableView/UICollectionView 在滚动时还会不会卡顿?

图片加载的工作流

概况来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流程如下:

假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,此时的图片并没有解压缩;然后将生成的 UIImage 赋值给 UIImageView接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化在主线程的下一个 RunLoop 到来时,Core Animation 提交了这个隐式的 Transaction,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤: 分配内存缓冲区用于管理文件 IO 和解压缩操作将文件数据从磁盘读取到内存中将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

由上面的步骤可知,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行。那么当需要加载图片较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现的更加突出。

为什么需要解压缩

既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。想要弄明白这个问题,我们需要知道什么是位图:

A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.

其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用的的 JPEG 和 PNG 图片就是位图。

下面是一张 PNG 图片,像素为 30 × 30,文件大小为 843B:

UIImage *image = [UIImage imageNamed:@"image"]; CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

就可以获取到这个图片的原始像素数据,大小为 3600B。

解压缩后的图片大小(3600) = 图片的像素宽(30) * 图片的像素高(30) * 每个像素所占的字节数(4)

事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0~100% 的压缩比。

因此,在将磁盘中的图片渲染到屏幕之前,必须要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片进行解压缩的原因。

强制解压缩

既然图片的解压缩不可避免,也不想让它在主线程执行,影响应用的响应性,那么是否有比较好的解决方案呢?答案是肯定的。

当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。

而强制解压缩的眼里就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数时 CGBitmapContextCreate:

/* Create a bitmap context. The context draws into a bitmap which is `width' pixels wide and `height' pixels high. The number of components for each pixel is specified by `space', which may also specify a destination color profile. Note that the only legal case when `space' can be NULL is when alpha is specified as kCGImageAlphaOnly.The number of bits for each component of a pixel is specified by `bitsPerComponent'. The number of bytes per pixel is equal to `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap consists of `bytesPerRow' bytes, which must be at least `width * bytes per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple of the number of bytes per pixel. `data', if non-NULL, points to a block of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the data for context is allocated automatically and freed when the context is deallocated. `bitmapInfo' specifies whether the bitmap should contain an alpha channel and how it's to be generated, along with whether the components are floating-point or integer. */ CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo) CG_AVAILABLE_STARTING(10.0, 2.0);

这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图。这个函数的注释比较长,参数也比较难理解,但是先别急,先来了解下相关知识,然后再回来理解这些参数,就比较简单了。

1. Pixel Format 像素格式

位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:

Bits per component: 一个像素中每个独立的颜色分量使用的 bit 数Bits per pixel: 一个像素使用的总 bit 数Bytes per row: 位图中的每一行使用的字节数

有一点需要注意的是,对于位图来说,像素格式并不是随意组合的,目前 Apple 平台支持 17 种格式: Pixel Format

2. Color and Color Spaces 颜色空间

什么是颜色空间呢? 在 Quartz 中,一个颜色是由一组值来表示的,比如(0,0,1)。而颜色空间是用来说明如何解析这些值的。

3. Color Spaces and Bitmap Layout 位图布局

像素格式是用来描述每个像素的组成格式的,比如每个像素使用的总 bit 数。而要想确保 Quartz 能够正确的解析这些 bit 所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo:

typedef CF_OPTIONS(uint32_t, CGBitmapInfo) { kCGBitmapAlphaInfoMask = 0x1F, kCGBitmapFloatInfoMask = 0xF00, kCGBitmapFloatComponents = (1


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3